package table;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import nodes.Node;
import nodes.Type;
import visitors.SymbolTablePrettyPrintVisitor;

public class SymbolTable
{
    public Node node;
    public SymbolTable parent;
    private ArrayList<SymbolTable> children;
    private Hashtable<String, List<Declaration>> table;

    public SymbolTable(Node node)
    {
        this.node = node;
        table = new Hashtable<String, List<Declaration>>();
        children = new ArrayList<SymbolTable>();
    }
    /**
     * Inserts the Declaration value into the SymbolTable.
     * 
     * @param key
     * @param value
     * @return True if value added with no conflicting entry.
     */
    public boolean insert(String key, Declaration value)
    {
        List<Declaration> tableValue = table.get(key);
        if (tableValue == null)
        {
            List<Declaration> newList = new LinkedList<Declaration>();
            newList.add(value);
            table.put(key, newList);
            return true;
        }
        boolean r = !tableValue.contains(value);
        if (value.getClass() == VariableDeclaration.class)
        {
            for (Declaration d : tableValue)
            {
                if (d.getClass() == VariableDeclaration.class)
                {
                    r = false;
                    break;
                }
            }
        }
        else if (value.getClass() == DataDeclaration.class)
        {
            for (Declaration d : tableValue)
            {
                if (d.getClass() == DataDeclaration.class)
                {
                    r = false;
                    break;
                }
            }
        }
        tableValue.add(value);
        return r;
    }

    /**
     * Performs local lookup of a variable with this name.
     *
     * @param name
     * @return
     */
    private VariableDeclaration localLookupVariableDeclaration(String name)
    {
        List<Declaration> list = table.get(name);
        if (list == null)
            return null;
        for (Declaration d : list)
        {
            if (d.getClass() == VariableDeclaration.class)
                return (VariableDeclaration) d;
        }
        return null;
    }

    /**
     * Performs a recursive lookup.
     *
     * @param name
     * @return
     */
    public VariableDeclaration lookupVariableDeclaration(String name)
    {
        return lookupVariableDeclaration(name, false);
    }

    /**
     * Performs a recursive or local lookup for a variable with this name.
     *
     * @param name
     * @param local
     * @return
     */
    public VariableDeclaration lookupVariableDeclaration(String name, boolean local)
    {
        if (local)
            return localLookupVariableDeclaration(name);
        VariableDeclaration vdec = localLookupVariableDeclaration(name);
        if (vdec == null && parent!=null)
        {
            return parent.lookupVariableDeclaration(name, local);
        }
        return vdec;
    }

    /**
     * Performs a lookup for a DataDeclaration.
     * Lookup is performed in the root scope,
     * because we know that data declarations can only be declared there.
     * Also, there can only be one declaration with this name.
     *
     * @param name Name of the data declaration
     * @return The data declaration, or null if undeclared
     */
    public DataDeclaration lookupDataDeclaration(String name)
    {
        if (parent != null)
        {
            return parent.lookupDataDeclaration(name);
        }
        List<Declaration> list = table.get(name);
        if (list == null)
            return null;
        for (Declaration d : list)
        {
            if (d.getClass() == DataDeclaration.class)
                return (DataDeclaration) d;
        }
        return null;
    }

    /**
     * Performs a lookup for a FunctionDeclaration 
     * that has same parameters as the passed FunctionDeclaration.
     * Lookup is performed in the root scope,
     * because we know that functions can only be declared there.
     *
     * @param name Name of the function
     * @param fdec Function Declaration
     * @return The function declaration, or null if undeclared
     */
    public FunctionDeclaration lookupFunctionDeclaration(String name, List<Type> params)
    {
        if (parent != null)
        {
            return parent.lookupFunctionDeclaration(name, params);
        }
        List<Declaration> list = table.get(name);
        if (list == null)
            return null;
        for (Declaration d : list)
        {
            if (d.getClass() == FunctionDeclaration.class)
            {
                FunctionDeclaration fdec = (FunctionDeclaration) d;
                if (fdec.hasParameters(params))
                    return fdec;
            }
        }
        return null;
    }

    public void replace(String key, Declaration value)
    {
        LinkedList<Declaration> tableValue = new LinkedList<Declaration>();
        tableValue.add(value);
        table.put(key, tableValue);
    }

    public SymbolTable beginScope(Node node)
    {
        SymbolTable child = new SymbolTable(node);
        child.parent = this;
        children.add(child);
        return child;
    }

    public SymbolTable endScope()
    {
        return parent;
    }

    public SymbolTable enterScope(Node node)
    {
        for (SymbolTable t : children)
        {
            if (t.node == node) // This is safe, we control the fact
                return t;       // that there can only be one scope per node
        }
        return null;
    }
    
    /**
     * Calling exitScope rather than endScope makes more intuitive sense.
     *
     * @return
     */
    public SymbolTable exitScope()
    {
        return endScope();
    }

    public void accept(SymbolTablePrettyPrintVisitor v)
    {
        v.visit(this);
    }

    public List<SymbolTable> getChildren()
    {
        return Collections.unmodifiableList(children);
    }

    public Map<String, List<Declaration>> getEntries()
    {
        Map<String, List<Declaration>> map = new Hashtable<String, List<Declaration>>();
        for (Map.Entry<String, List<Declaration>> e : table.entrySet())
        {
            List<Declaration> list = new LinkedList<Declaration>();
            for (Declaration d : e.getValue())
            {
                list.add(d);
            }
            map.put(e.getKey(), Collections.unmodifiableList(list));
        }
        return Collections.unmodifiableMap(map);
    }

    public SymbolTable getParent()
    {
        return parent;
    }

    public Node getNode()
    {
        return node;
    }
}